import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SliderModule } from './slider.module';
import { CommonModule } from '@angular/common';
import { InputModule } from '../input/input.module';
import { TimePickerModule } from '../time-picker/time-picker.module';
import { SliderComponent } from './slider.component';
import { IconModule } from '../icon/icon.module';

interface MouseEventWithOffsets extends MouseEventInit {
  pageX?: number;
  pageY?: number;
  offsetX?: number;
  offsetY?: number;
  x?: number;
  y?: number;
}

class FakeMouseEvent extends MouseEvent {
  constructor(type: string, values: MouseEventWithOffsets) {
    const { pageX, pageY, offsetX, offsetY, x, y, ...mouseValues } = values;
    super(type, mouseValues);

    Object.assign(this, {
      offsetX: offsetX || 0,
      offsetY: offsetY || 0,
      pageX: pageX || 0,
      pageY: pageY || 0,
      x: x || 0,
      y: y || 0,
    });
  }
}

@Component({
  template: `
    <lv-slider
      [(ngModel)]="sliderValue"
      [lvPoints]="points"
      [lvRange]="range"
      [lvStep]="step"
      [lvDirection]="direction"
      [lvReverse]="reverse"
      [lvTrackVisible]="trackVisible"
      [lvHandleColor]="handleColor"
      [lvMin]="0"
      [lvMax]="100"
      lvDivision="3"
      [lvDisabled]="disabled"
      (onChange)="onChange()"
      (onSlideEnd)="onSliderEnd($event)"
    ></lv-slider>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestSliderComponent {
  sliderValue = 70;
  range = false;
  step = 1;
  disabled = false;
  direction = 'horizontal';
  reverse = false;
  trackVisible = true;
  handleColor;
  points = [
    {
      value: 0,
      label: '0',
    },
    {
      value: 50,
      label: '50',
    },
    {
      value: 100,
      label: '100',
    },
  ];
  onChange() {}
  onSliderEnd() {}
}

describe('SliderComponent', () => {
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [TestSliderComponent],
      imports: [SliderModule, FormsModule, NoopAnimationsModule],
      teardown: { destroyAfterEach: false },
    }).compileComponents();
  }));

  describe('render', () => {
    let component: TestSliderComponent;
    let fixture: ComponentFixture<TestSliderComponent>;
    const renderComp = fakeAsync(para => {
      for (const [key, value] of Object.entries(para)) {
        component[key] = value;
      }
      fixture.detectChanges();
      tick(1500);
      fixture.detectChanges();
      expect(fixture).toMatchSnapshot();
    });

    beforeEach(() => {
      fixture = TestBed.createComponent(TestSliderComponent);
      component = fixture.componentInstance;
    });

    it('with lvRange', () => {
      renderComp({ lvRange: true, sliderValue: [30, 70] });
    });

    it('with lvDirection', () => {
      renderComp({ direction: 'vertical' });
    });

    it('with lvTrackVisible', () => {
      renderComp({ lvTrackVisible: 'false' });
    });

    it('with lvHandleColor', () => {
      renderComp({
        lvHandleColor: [
          [0, 'red'],
          [50, 'green'],
          [100, 'yellow'],
        ],
      });
    });

    it('with lvDisabled', () => {
      renderComp({ lvDisabled: true });
    });
  });
});

@Component({
  template: `
    <div class="container">
      <span class="slider">
        <lv-slider
          #slider
          [(ngModel)]="val"
          [lvLines]="lines"
          [lvPoints]="points"
          [lvTooltip]="tipTemp"
          [lvMin]="0"
          [lvMax]="86400"
          [lvTrackVisible]="false"
          [lvHoverShowTooltip]="true"
          (onChange)="onChange($event)"
          (onSlideEnd)="onSlideEnd($event)"
        >
          <ng-template #tipTemp let-data>{{ valToTime(data) }}</ng-template>
        </lv-slider>
      </span>
      <lv-time-picker [(ngModel)]="timeValue" (ngModelChange)="timeChange($event)" lvPlaceholder="HH:mm:ss"></lv-time-picker>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestSliderFuncComponent {
  val;
  lines = [];
  points = [];
  realVal;
  timeValue = new Date();
  @ViewChild('slider') slider: SliderComponent;
  ngOnInit() {
    this.lines = [
      {
        value: [14400, 21600],
        style: {
          'background-color': '#2673e5',
        },
      },
      {
        value: [43200, 72000],
        style: {
          'background-color': '#2673e5',
        },
      },
    ];
    this.points = [
      {
        value: this.calculatePoint(0),
        label: '00:00:00',
      },
      {
        value: this.calculatePoint(1),
        label: '04:00:00',
        pointStyle: {
          'border-color': '#2673e5',
        },
      },
      {
        value: this.calculatePoint(2),
        label: '08:00:00',
        textStyle: {
          color: '#2673e5',
        },
      },
      {
        value: this.calculatePoint(3),
        label: '12:00:00',
        pointStyle: {
          'border-color': '#2673e5',
          'border-radius': '0',
        },
        textStyle: {
          color: '#2673e5',
          'font-size': '16px',
        },
      },
      {
        value: this.calculatePoint(4),
        label: '16:00:00',
      },
      {
        value: this.calculatePoint(5),
        label: '20:00:00',
      },
      {
        value: this.calculatePoint(6),
        label: '24:00:00',
      },
    ];
  }

  calculatePoint(index) {
    return 3600 * 4 * index;
  }

  //value与时间换算
  valToTime(val) {
    const time = val;
    const min = Math.floor(time % 3600);
    return `${this.formatTime(Math.floor(time / 3600))}:${this.formatTime(Math.floor(min / 60))}:${this.formatTime(time % 60)}`;
  }

  valToDatetime(val) {
    const date = new Date();
    const arr = val.split(':');
    const currentHours = arr[0];
    const currentMinutes = arr[1];
    const currentSeconds = arr[2];
    date.setHours(Number(currentHours));
    date.setMinutes(Number(currentMinutes));
    date.setSeconds(Number(currentSeconds));
    return date;
  }

  formatTime(time) {
    if (time < 10) {
      return `0${time}`;
    } else {
      return time;
    }
  }

  // slider值改变时触发事件
  onChange = event => {
    this.realVal = this.valToTime(this.val);
    this.timeValue = this.valToDatetime(this.realVal); //转换成时间组件能显示的时间格式
  };

  // 时分秒改变
  timeChange(realVal) {
    if (!realVal) {
      return;
    }
    const time = realVal.getHours() * 3600 + realVal.getMinutes() * 60 + realVal.getSeconds();
    this.val = time;
  }
}

describe('SliderFuncComponent', () => {
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [TestSliderFuncComponent],
      imports: [CommonModule, FormsModule, SliderModule, InputModule, TimePickerModule, NoopAnimationsModule],
      teardown: { destroyAfterEach: true },
    }).compileComponents();
  }));

  describe('render', () => {
    let component: TestSliderFuncComponent;
    let fixture: ComponentFixture<TestSliderFuncComponent>;
    const renderComp = fakeAsync(para => {
      for (const [key, value] of Object.entries(para)) {
        component[key] = value;
      }
      fixture.detectChanges();
      tick(500);
      fixture.detectChanges();
      expect(fixture).toMatchSnapshot();
      tick(1500);
    });

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(TestSliderFuncComponent);
      component = fixture.componentInstance;
      fixture.detectChanges();
      tick(500);
    }));

    it('basic', () => {
      renderComp({});
    });
  });

  describe('function', () => {
    let fixture: ComponentFixture<TestSliderFuncComponent>;
    let component: TestSliderFuncComponent;
    let instance: SliderComponent;
    const detectChange = () => {
      fixture.detectChanges();
      tick(500);
      fixture.detectChanges();
      tick(500);
      fixture.detectChanges();
      tick(500);
    };

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(TestSliderFuncComponent);
      component = fixture.componentInstance;
      detectChange();
      instance = component.slider;
      const dom = instance.el.nativeElement.children[0];
      jest.spyOn(dom, 'offsetWidth', 'get').mockReturnValue(100);
      jest.spyOn(dom, 'offsetHeight', 'get').mockReturnValue(100);
    }));

    it('backFill', fakeAsync(() => {
      const timeInput = document.querySelector('input');
      timeInput.dispatchEvent(new Event('click'));
      const testTime = (time: string) => {
        timeInput.value = time;
        timeInput.dispatchEvent(new Event('input'));
        detectChange();
        expect(fixture).toMatchSnapshot();
      };
      testTime('16:00:00');
      testTime('00:00:00');
      testTime('23:59:59');
    }));

    it('click', () => {
      expect(instance.el.nativeElement.children[0].offsetWidth).toBe(100);
      expect(instance.el.nativeElement.children[0].offsetHeight).toBe(100);
      instance.onBarClick({ pageX: 40 });
      expect(instance.values[0]).toBe(86400 * 0.4);
    });

    it('getHandleColor', fakeAsync(() => {
      instance.lvHandleColor = [
        [70, '#fff'],
        [50, '#000'],
      ];
      expect(instance.getHandleColor(40)).toBeNull();
      expect(instance.getHandleColor(50)).toBe('#000');
    }));

    it('onPointClick', fakeAsync(() => {
      document.querySelector('.lv-slider-range-points div:nth-child(5)').dispatchEvent(new Event('click'));
      detectChange();
      expect(fixture).toMatchSnapshot();
    }));

    const getHandle = () => document.querySelector('.lv-slider-handle') as HTMLElement;

    it('drag', fakeAsync(() => {
      getHandle().dispatchEvent(new MouseEvent('mousedown'));
      detectChange();
      document.dispatchEvent(new FakeMouseEvent('mousemove', { pageX: 40 }));
      detectChange();
      document.dispatchEvent(new MouseEvent('mouseup'));
      detectChange();
      expect(instance.values[0]).toBe(86400 * 0.4);
    }));

    it('arrowKeyDown', fakeAsync(() => {
      const testArrow = (key: number, value: number) => {
        getHandle().dispatchEvent(
          new KeyboardEvent('keydown', {
            which: key,
          })
        );
        detectChange();
        expect(instance.values[0]).toBe(value);
      };
      testArrow(37, 0);
      testArrow(39, 1);
      testArrow(38, 2);
      testArrow(40, 1);
    }));

    it('tooltip', fakeAsync(() => {
      getHandle().dispatchEvent(new MouseEvent('mouseenter'));
      detectChange();
      expect(fixture).toMatchSnapshot();
      getHandle().dispatchEvent(new MouseEvent('mouseleave'));
      detectChange();
      expect(fixture).toMatchSnapshot();
      const rail = document.querySelector('.lv-slider-rail');
      rail.dispatchEvent(new MouseEvent('mouseenter'));
      detectChange();
      expect(fixture).toMatchSnapshot();
      rail.dispatchEvent(new FakeMouseEvent('mousemove', { pageX: 40 }));
      detectChange();
      expect(fixture).toMatchSnapshot();
      rail.dispatchEvent(new MouseEvent('mouseleave'));
      detectChange();
      expect(fixture).toMatchSnapshot();
    }));
  });
});

interface HourMinSecond {
  hour: number;
  min: number;
  second: number;
}

const date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);

@Component({
  template: `
    <div class="container">
      <span class="slider">
        <lv-slider
          #slider
          [(ngModel)]="sliderValue"
          [lvRange]="true"
          [lvPoints]="points"
          [lvTooltip]="tipTemp"
          [lvMin]="0"
          [lvMax]="86399"
          [lvHoverShowTooltip]="true"
          (onChange)="onChange()"
          (onSlideEnd)="onSliderEnd($event)"
        >
          <ng-template #tipTemp let-data>{{ valueToTimeStr(data) }}</ng-template>
        </lv-slider>
      </span>
      <lv-time-picker
        [(ngModel)]="startTimeValue"
        (ngModelChange)="timeChange()"
        lvPlaceholder="HH:mm"
        [lvFormat]="'HH:mm'"
        (lvOpenChange)="openChange()"
      ></lv-time-picker>
      <span class="separator">-</span>
      <lv-time-picker
        [(ngModel)]="endTimeValue"
        (ngModelChange)="timeChange()"
        lvPlaceholder="HH:mm"
        [lvFormat]="'HH:mm'"
        (lvOpenChange)="openChange()"
      ></lv-time-picker>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestSliderRangeFuncComponent {
  points: { value: number; label: string }[] = [];
  startTimeValue: Date = date;
  endTimeValue: Date = new Date(this.startTimeValue.getTime() + 60 * 60 * 1000);
  sliderValue: number[] = [];
  @ViewChild('slider') slider: SliderComponent;
  ngOnInit() {
    this.timeChange();

    this.points = [
      {
        value: this.calculatePoint(0),
        label: '00:00',
      },
      {
        value: this.calculatePoint(1),
        label: '06:00',
      },
      {
        value: this.calculatePoint(2),
        label: '12:00',
      },
      {
        value: this.calculatePoint(3),
        label: '18:00',
      },
      {
        value: this.calculatePoint(4),
        label: '23:59',
      },
    ];
  }

  calculatePoint(index: number): number {
    if (index === 4) {
      return 3600 * 6 * index - 1;
    }
    return 3600 * 6 * index;
  }

  // slider值改变时触发事件
  onChange = () => {
    this.startTimeValue = this.valueToDateTime(this.sliderValue[0]); //转换成时间组件能显示的时间格式
    this.endTimeValue = this.valueToDateTime(this.sliderValue[1]);
  };

  //value转化成时分字符串显示
  valueToTimeStr(value: number): string {
    const time: HourMinSecond = this.getHourMinSecond(value);
    return `${this.formatTime(time.hour)}:${this.formatTime(time.min)}`;
  }

  formatTime(time: number): string {
    if (time < 10) {
      return `0${time}`;
    } else {
      return String(time);
    }
  }

  getHourMinSecond(value: number): HourMinSecond {
    const hour = Math.floor(value / 3600);
    const min = Math.floor((value % 3600) / 60);
    const second = Math.floor(value % 60);
    return { hour, min, second };
  }

  valueToDateTime(value: number): Date {
    const date = new Date();
    const time = this.getHourMinSecond(value);
    date.setHours(time.hour);
    date.setMinutes(time.min);
    date.setSeconds(time.second);
    return date;
  }

  // 时分秒改变
  timeChange(): void {
    if (this.startTimeValue && this.endTimeValue) {
      this.sliderValue = [this.timeToValue(this.startTimeValue), this.timeToValue(this.endTimeValue)];
    }
  }

  timeToValue(value: Date): number {
    return value.getHours() * 3600 + value.getMinutes() * 60 + value.getSeconds();
  }

  openChange() {
    if (this.startTimeValue > this.endTimeValue) {
      [this.startTimeValue, this.endTimeValue] = [this.endTimeValue, this.startTimeValue];
    }
  }

  onSliderEnd() {}
}

describe('SliderRangeFuncComponent', () => {
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [TestSliderRangeFuncComponent],
      imports: [CommonModule, FormsModule, SliderModule, IconModule, TimePickerModule, NoopAnimationsModule],
      teardown: { destroyAfterEach: true },
    }).compileComponents();
  }));

  describe('render', () => {
    let component: TestSliderRangeFuncComponent;
    let fixture: ComponentFixture<TestSliderRangeFuncComponent>;
    const renderComp = fakeAsync(para => {
      for (const [key, value] of Object.entries(para)) {
        component[key] = value;
      }
      fixture.detectChanges();
      tick(500);
      fixture.detectChanges();
      expect(fixture).toMatchSnapshot();
      tick(1500);
    });

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(TestSliderRangeFuncComponent);
      component = fixture.componentInstance;
      fixture.detectChanges();
      tick(500);
    }));

    it('basic', () => {
      renderComp({});
    });
  });

  describe('function', () => {
    let fixture: ComponentFixture<TestSliderRangeFuncComponent>;
    let component: TestSliderRangeFuncComponent;
    let instance: SliderComponent;
    const detectChange = () => {
      fixture.detectChanges();
      tick(500);
      fixture.detectChanges();
      tick(500);
      fixture.detectChanges();
      tick(500);
    };

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(TestSliderRangeFuncComponent);
      component = fixture.componentInstance;
      detectChange();
      instance = component.slider;
      const dom = instance.el.nativeElement.children[0];
      jest.spyOn(dom, 'offsetWidth', 'get').mockReturnValue(100);
      jest.spyOn(dom, 'offsetHeight', 'get').mockReturnValue(100);
    }));

    it('backfill', fakeAsync(() => {
      const [startInput, endInput] = Array.from(document.querySelectorAll('input'));
      const testTime = (input: HTMLInputElement, time: string) => {
        input.value = time;
        input.dispatchEvent(new Event('input'));
      };
      testTime(endInput, '17:00');
      testTime(startInput, '16:00');
      detectChange();
      expect(fixture).toMatchSnapshot();
    }));

    it('drag', fakeAsync(() => {
      const [handleLeft, handleRight] = Array.from(document.querySelectorAll('.lv-slider-handle') as NodeListOf<HTMLElement>);
      const drag = (handle: HTMLElement, value: number) => {
        handle.dispatchEvent(new MouseEvent('mousedown'));
        detectChange();
        document.dispatchEvent(new FakeMouseEvent('mousemove', { pageX: value }));
        detectChange();
        document.dispatchEvent(new MouseEvent('mouseup'));
        detectChange();
      };
      drag(handleRight, 40);
      drag(handleLeft, 20);
      expect(instance.values[0]).toBe(Math.floor(86399 * 0.2));
      expect(instance.values[1]).toBe(Math.floor(86399 * 0.4));
    }));

    it('click', fakeAsync(() => {
      const clickPoint = (nth: number) => {
        document.querySelector(`.lv-slider-range-points div:nth-child(${nth})`).dispatchEvent(new Event('click'));
        detectChange();
        expect(fixture).toMatchSnapshot();
      };
      clickPoint(5);
      clickPoint(3);
      clickPoint(1);
    }));
  });
});
